package com.oig.sys.query;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.oig.auth.api.ResourceDataCfgService;
import com.oig.auth.api.ResourceService;
import com.oig.base.api.EnumService;
import com.oig.common.exception.BadRequestException;
import com.oig.common.exception.ServiceException;
import com.oig.common.module.SysPageInfo;
import com.oig.common.util.JsonUtil;
import com.oig.common.util.RequestHeaderUtil;
import com.oig.sys.auth.constants.AuthEnums;
import com.oig.sys.auth.dto.ResourceGQLCondDto;
import com.oig.sys.auth.vo.ResourceColumnVo;
import com.oig.sys.auth.vo.ResourceVo;
import com.oig.sys.auth.vo.UserDataCfgVo;
import com.oig.sys.minio.core.MinIOTemplate;
import com.oig.sys.msg.api.ExportTaskService;
import com.oig.sys.msg.dto.ExportTaskDto;
import com.oig.sys.msg.vo.ExportTaskVo;
import com.oig.sys.query.constants.ExpressionEnum;
import com.oig.sys.query.support.MongoExpConditionParser;
import com.oig.sys.query.support.module.ConditionExp;
import com.oig.sys.query.support.module.ConditionPropertyExp;
import com.oig.sys.security.config.CacheConstants;
import com.oig.sys.security.support.SecurityUser;
import com.oig.sys.security.util.SecurityUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Tag(name = "通用查询")
@Slf4j
@RestController
@RequiredArgsConstructor
public class SysCommQueryEndpoint {

    private final SysCommQueryService queryService;
    private final ResourceService resourceService ;
    private final ResourceDataCfgService resourceDataCfgService;
    private final ExportTaskService exportTaskService;
    private final EnumService enumService;
    private final MinIOTemplate minIOTemplate;
    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;



    @Operation(summary = "通用查询")
    @PreAuthorize("@pms.hasPermission('/queryForPage/'+ #resourceCode +'')")
    @PostMapping("/queryForPage/{resourceCode}")
    public SysPageInfo<Map<String,Object>> queryForPage(@PathVariable(value = "resourceCode")String resourceCode, @RequestBody SysCommQueryReq queryReq){
        log.info("queryForList resourceCode : {}, common query condition : {}",resourceCode,queryReq) ;
        ResourceVo vo = this.buildQuery(resourceCode, queryReq);
        return queryService.queryForPage(vo.getJsonText(), queryReq);
    }


    private ResourceVo buildQuery(String resourceCode, SysCommQueryReq queryReq){
        SecurityUser securityUser = SecurityUtil.getUserForIfNullBuild();
        ResourceVo vo = resourceService.getByCode(resourceCode, securityUser.getId()) ;
        if (vo==null){
            throw new BadRequestException("菜单编码错误") ;
        }
        if (StringUtils.isBlank(vo.getJsonText())){
            throw new BadRequestException("查询描述未配置") ;
        }
        //如果是系统调用，那么应该要带上，tenant参数，否则可能出现越权。
        if (securityUser.getId()==0 && !AuthEnums.DataScope.PUB.getCode().equals(vo.getIsPublicData())){
            if (MongoExpConditionParser.checkAndCondition(queryReq.getWhere(), CacheConstants.USER_TENANT_ID)){
                throw new BadRequestException("不能越租户查询数据") ;
            }
        }
        // 1、系统租户隔离
        ConditionExp cexp = null;
        // 数据权限过滤部分
        if (securityUser.getTenantId()!=0){   //超级管理员不需要做数据隔离
            if (AuthEnums.DataScope.TAN.getCode().equals(vo.getIsPublicData())){
                if (vo.getJsonText().contains(CacheConstants.USER_TENANT_ID)){
                    cexp=MongoExpConditionParser.createAndCondition(CacheConstants.USER_TENANT_ID, ExpressionEnum.MongoSymbolEnum.IN.getMongoStr(),securityUser.getTenantId());
                } else {
                    throw new BadRequestException("无所属租户字段，无法按租户查询");
                }
            }
        }
        // 2、默认查询条件
        if (StringUtils.isNotBlank(vo.getQueryConditionCfg())){
            log.debug("default QueryConditionCfg:{}",vo.getQueryConditionCfg());
            List<ResourceGQLCondDto> condList = JsonUtil.toList(vo.getQueryConditionCfg(),ResourceGQLCondDto.class) ;
            for(ResourceGQLCondDto condDto : condList){
                if (cexp == null){
                    cexp = MongoExpConditionParser.createAndCondition(condDto.getFieldName(), condDto.getSymbol(), condDto.getValue());
                } else {
                    cexp.getExpList().add(new ConditionPropertyExp(condDto.getFieldName(), condDto.getSymbol(), condDto.getValue()));
                }
            }
        }
        if (securityUser.getTenantId()!=0) {   //超级管理员不需要做数据权限
            // 3、数据权限配置
            UserDataCfgVo userDataCfgVo = resourceDataCfgService.getUserDataCfg(vo.getId());
            if (userDataCfgVo == null) {
                throw new ServiceException("auth服务不可用");
            }
            if (userDataCfgVo.getEqTypeFields() != null) {
                log.debug("userDataCfgVo:{}",userDataCfgVo);
                List<String> fieldList = userDataCfgVo.getEqTypeFields().get(AuthEnums.DataCfgEqType.USERID.getCode());
                for (String property : fieldList) {
                    if (cexp == null) {
                        cexp = MongoExpConditionParser.createAndCondition(property, ExpressionEnum.MongoSymbolEnum.EQ.getMongoStr(), securityUser.getId());
                    } else {
                        cexp.getExpList().add(new ConditionPropertyExp(property, ExpressionEnum.MongoSymbolEnum.EQ.getMongoStr(), securityUser.getId()));
                    }
                }
            }
            if (userDataCfgVo.getEqFieldValue()!=null){
                log.debug("固定值权限过滤：对应范围的用户能查询到按这个对应字段匹配值的单据");
                for (String property : userDataCfgVo.getEqFieldValue().keySet()){
                    if (cexp == null) {
                        cexp = MongoExpConditionParser.createAndCondition(property, ExpressionEnum.MongoSymbolEnum.EQ.getMongoStr(), userDataCfgVo.getEqFieldValue().get(property));
                    } else {
                        cexp.getExpList().add(new ConditionPropertyExp(property, ExpressionEnum.MongoSymbolEnum.EQ.getMongoStr(), userDataCfgVo.getEqFieldValue().get(property)));
                    }
                }
            }
            if (userDataCfgVo.getExcludeFields()!=null){    //排除的列
                queryReq.setColumnPropertyList(userDataCfgVo.getExcludeFields());
            }
        }
        queryReq.setInnerWhere(cexp);
        // 默认排序配置
        if (StringUtils.isNotBlank(vo.getQuerySortCfg()) && CollectionUtil.isEmpty(queryReq.getSort())){
            log.debug("QuerySortCfg:{}", vo.getQuerySortCfg());
            Map<String, Object> sortMap = JsonUtil.stringToCollect(vo.getQuerySortCfg()) ;
            for(String key : sortMap.keySet()){
                queryReq.getSort().put(key, String.valueOf(sortMap.get(key))) ;
            }
        }
        return vo;
    }

    /**
     * 加了导出excel，所有的查询服务就需要依赖minio， 还要配置spring线程池
     * 改成配置了通用查询就有导出权限
     */
    @Operation(summary = "通用导出excel")
    @PreAuthorize("@pms.hasPermission('/queryForPage/'+ #resourceCode +'')")
    @PostMapping("/exportExcel/{resourceCode}")
    public ExportTaskVo exportExcel(@PathVariable(value = "resourceCode")String resourceCode, @RequestBody SysCommQueryReq queryReq){
        log.info("exportExcel resourceCode : {}, common query condition : {}",resourceCode,queryReq) ;
        ResourceVo vo = this.buildQuery(resourceCode, queryReq);
        //1、添加任务到队列 以便前端查询正在进行的任务
        ExportTaskVo exportTaskVo = this.addExportToTask(vo);
        if (exportTaskVo==null){
            throw new BadRequestException("导出任务添加失败");
        }
        String token = RequestHeaderUtil.getToken() ;
        //2、直接使用spring本身的线程池来处理异步生成文件， 这种方式限制在线程数，如果使用发消息那里那样搞固定几个线程去循环任务，需要序列华存储查询条件，先用这种方式
        threadPoolTaskExecutor.execute(new Thread(()->this.createExcel(vo, queryReq, exportTaskVo, token)));
        //3、返回前端任务信息
        return exportTaskVo;
    }


    private ExportTaskVo addExportToTask(ResourceVo vo){
        ExportTaskDto exportTaskDto = new ExportTaskDto();
        exportTaskDto.setCreateUserId(SecurityUtil.getDefineUserMain().getId());
        exportTaskDto.setCreateUserName(SecurityUtil.getDefineUserMain().getRealName());
        exportTaskDto.setTenantId(SecurityUtil.getDefineUserMain().getTenantId());
        exportTaskDto.setTenantName(SecurityUtil.getDefineUserMain().getTenantName());
        exportTaskDto.setName(vo.getResourceName()+"-导出excel");
        return exportTaskService.addExportTask(exportTaskDto) ;
    }

    /**
     * 异步任务
     */
    private void createExcel(ResourceVo vo, SysCommQueryReq queryReq, ExportTaskVo taskVo, String token){
        //1、查询数据
        queryReq.setPage(new ArrayList<Integer>(){{add(0);add(500);}});
        SysPageInfo<Map<String, Object>> pageData = queryService.queryForPage(vo.getJsonText(), queryReq);
        ExcelWriter writer = null;
        OutputStream out = null;
        try{
            //2、写excel
            writer = ExcelUtil.getBigWriter();
            for (ResourceColumnVo columnVo : vo.getResourceColumnList()){
                if (columnVo.getIsShowFlag()){
                    writer.addHeaderAlias(columnVo.getColumnField(), columnVo.getColumnName());
                }
            }
            File tempFile = File.createTempFile(vo.getResourceName()+"-导出-"+System.currentTimeMillis(), ".xlsx");
            log.debug(tempFile.getName());
            log.debug(tempFile.getAbsolutePath());
            out = Files.newOutputStream(tempFile.toPath());
            //写入数据，map第一个key作为表头
            if (CollectionUtil.isNotEmpty(pageData.getList())){
                Map<String, String> colEnumMap = vo.getResourceColumnList().stream()
                        .filter(itm->StringUtils.isNotBlank(itm.getEnumValueJson()))
                        .collect(Collectors.toMap(ResourceColumnVo::getColumnField,ResourceColumnVo::getEnumValueJson));
                log.debug("colEnumMap:{}", colEnumMap);
                Map<String,Map<String, String>> enItemMap = enumService.getAllEnumItem();
                if (!colEnumMap.isEmpty() && !enItemMap.isEmpty()){
                    this.changeEnum(colEnumMap, enItemMap, pageData.getList());
                }
                writer.setOnlyAlias(true);
                writer.write(pageData.getList(), true) ;    //枚举数据没有处理
                if (pageData.isHasNextPage()){
                    queryReq.setPage(this.incPageNum(pageData));
                    this.loopWriteData(colEnumMap, enItemMap, writer, vo, queryReq);
                }
            }
            writer.flush(out);
            // 关闭writer，释放内存
            writer.close();
            writer = null;
            //3、excel上传到minio
            String objPath = minIOTemplate.putObject(tempFile.getName(), Files.newInputStream(tempFile.toPath()));
            log.info("down task file:{}",objPath);
            taskVo.setFileName(tempFile.getName());
            //taskVo.setFileUrl(objPath); //完整地址 待拼接
            taskVo.setFileId(objPath);
            //4、更新任务状态和文件地址并发送消息
            this.updateTaskAndNotify(taskVo, token);
            FileUtils.deleteQuietly(tempFile);
        } catch (IOException e){
            log.error("createFile error..", e);
        } catch (Exception ex){
            log.error("createExcel error..", ex);
        } finally {
            if (writer!=null){
                writer.close();
            }
            if (out!=null){
                IOUtils.closeQuietly(out);
            }
        }
    }

    private void updateTaskAndNotify(ExportTaskVo taskVo, String token){
        ExportTaskDto exportTaskDto = new ExportTaskDto();
        BeanUtil.copyProperties(taskVo, exportTaskDto);
        this.exportTaskService.updateTaskAndNotify(exportTaskDto, token);

    }

    private List<Integer> incPageNum(SysPageInfo<Map<String, Object>> pageData){
        List<Integer> limit = new ArrayList<>();
        limit.add(pageData.getPageNum()*pageData.getPageSize());    //起始位置
        limit.add(pageData.getPageSize());                              //行数
        return limit;
    }

    private void loopWriteData(Map<String, String> colEnumMap, Map<String,Map<String, String>> enItemMap, ExcelWriter writer, ResourceVo vo, SysCommQueryReq queryReq){
        SysPageInfo<Map<String, Object>> pageData = queryService.queryForPage(vo.getJsonText(), queryReq);
        if (!colEnumMap.isEmpty() && !enItemMap.isEmpty()){
            this.changeEnum(colEnumMap, enItemMap, pageData.getList());
        }
        writer.write(pageData.getList());
        if (pageData.isHasNextPage()){
            queryReq.setPage(this.incPageNum(pageData));
            this.loopWriteData(colEnumMap, enItemMap, writer, vo, queryReq);
        }
    }

    /**
     * 枚举值替换
     * @param colEnumMap 配置了枚举的列
     * @param enItemMap 全部枚举
     * @param dataList 数据
     */
    private void changeEnum(Map<String, String> colEnumMap, Map<String,Map<String, String>> enItemMap, List<Map<String, Object>> dataList){
        dataList.forEach( itm ->  itm.forEach((col, value)->{
            if (colEnumMap.containsKey(col)) {  //是枚举字段
                Map<String,String> dictItemMap = enItemMap.get(colEnumMap.get(col));
                if (dictItemMap!=null && value!=null){
                    String nameV = dictItemMap.get(String.valueOf(value));
                    if (nameV!=null){
                        itm.put(col, nameV);    //这个列换成枚举名称
                    }
                }
            }
        }));
    }

}
